#include "shnet.h"
#include <QDataStream>
#include <QDebug>
#include "propertymapper.h"
#include "dynamicqobject.h"
#include "propertyfilter.h"
#include "signalfilter.h"

ShNet::ShNet(QObject *parent) :
    QObject(parent)
{
    m_udp = new QUdpSocket(this);
    connect(m_udp, &QUdpSocket::readyRead,
            this, &ShNet::recievePendingDatagrams);
    m_propertyMapper = new PropertyMapper(this, "property_changed", this);
    m_dynamicQObject = new DynamicQObject(this, "signal_catched", this);
    m_propertyFilter = new PropertyFilter();
    m_signalFilter = new SignalFilter(this);
    m_bindedMapper = new PropertyMapper(this, "binded_property_changed", this);
}

ShNet::~ShNet()
{
    delete m_propertyFilter;
}

//
// setup & settings
//
bool ShNet::join(const QHostAddress &interface,
                 const QHostAddress &multicastGroup, quint16 port,
                 const QString &nodeName, const QStringList &nodeGroups,
                 const QVariantMap &nodeInfo)
{
    if (!m_udp->bind(interface, port,
                     QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint)) {
        qCritical() << "bind failed";
        return false;
    }

    if (m_udp->state() != QUdpSocket::BoundState) {
        qCritical() << "socket is not in bound state";
        return false;
    }

    if (!m_udp->joinMulticastGroup(multicastGroup)) {
        qWarning() << "can't join multicast group, working broadcast";
        m_groupAddress = QHostAddress("255.255.255.255");
    }

    m_nodeName = nodeName;
    m_nodeGroups = nodeGroups;
    m_nodeInfo = nodeInfo;
    m_groupAddress = multicastGroup;
    m_port = port;

    return true;
}

//
// networking
//
void ShNet::recievePendingDatagrams()
{
    while (m_udp->hasPendingDatagrams()) {
        QByteArray datagram;
        datagram.resize(m_udp->pendingDatagramSize());
        m_udp->readDatagram(datagram.data(), datagram.size());
        processDatagram(datagram);
    }
}

void ShNet::processDatagram(QByteArray &datagram)
{
    QDataStream in(&datagram, QIODevice::ReadOnly);
    in.setVersion(QDataStream::Qt_5_0);

    // check version
    quint8 protocolVersion;
    in >> protocolVersion;
    if (protocolVersion != 1) {
        qWarning() << "recieved unsupported frame with version:" << protocolVersion;
        return;
    }

    // ignore our frames
    QString sender_name;
    in >> sender_name;
    if (sender_name == m_nodeName)
        return;

    // process frame
    quint8 frame_type;
    in >> frame_type;
    FRAME_TYPE frameType = (FRAME_TYPE)frame_type;

    Url url;
    in >> url;

    if (frameType == FRAME_TYPE::PROPERY_CHANGE_NOTIFY) {
        m_propertyFilter->filter(url);
        return;
    }

    if (frameType == FRAME_TYPE::NETWORK_SIGNAL) {
        QVariantList parameters;
        in >> parameters;
        m_signalFilter->filter(url, parameters);
        return;
    }

    if (frameType == FRAME_TYPE::FUNCTION_CALL_RESULT) {

        Url ret;
        in >> ret;
        qDebug() << "f ret:" << ret.toString();
        return;
    }

    if (!url.isTargetNet()) {
        qWarning() << "can't process url, i'm not a gateway:" << url.toString();
        return;
    }

    UrlNode node = url.currentNode();
    if (UrlNode::match(node, m_nodeName, m_nodeGroups)) { // if url for us

        if (!url.next()) {
            qDebug() << "incomplete Url (nothing to exec):" << url.toString();
            return;
        }
    } else {
        return;
    }

    switch (frameType) {
    case FRAME_TYPE::FUNCTION_CALL:
        function_call(url.currentNode(), url.argumentData(), sender_name);
        break;
    case FRAME_TYPE::PROPERTY_SET:
        set_property(url.currentNode(), url.argumentData());
        break;
    default:
        break;
    }
}

//
// properties
//
void ShNet::setProperty(const Url &propertyUrl)
{
    sh_net_startmsg();
    out << (quint8)FRAME_TYPE::PROPERTY_SET;
    out << propertyUrl;
    sh_net_sendmsg();
}

void ShNet::setProperty(const QString &netName, const QVariant &value)
{
    m_propertyMapper->setMappedProperty(netName, value);
}

QVariant ShNet::getProperty(const QString &name) const
{
    return m_propertyMapper->mappedProperty(name);
}

void ShNet::addProperty(const QString &netName,
                        bool isQuickProperty,
                        QObject *object,
                        const char *objectPropertyName,
                        const QStringList &netGroups,
                        const QVariantMap &info)
{
    if (m_properties.contains(netName)) {
        qWarning() << "node" << m_nodeName
                   << "already has" << netName << "property";
        return;
    }
    m_properties.insert(netName, {netGroups, info});
    m_propertyMapper->addProperty(netName,
                                  objectPropertyName,
                                  object,
                                  isQuickProperty);
}

void ShNet::bindProperty(const Url &bindTo,
                         QObject *object, const char *propertyName,
                         bool isQuickProperty)
{
    QString propertyStr(propertyName);

    if (m_bindedProperties.contains(propertyStr)) {
        qWarning() << "can't bind property" << propertyName << "already binded";
        return;
    }

    int id = m_bindedMapper->addProperty(propertyStr,
                                         propertyName, object, isQuickProperty);
    if (id == -1) {
        qWarning() << "can't bind property" << propertyName;
        return;
    }

    propertyFilter()->add(bindTo, PropertyFilter::PROPERTY_SET, m_bindedMapper, propertyName);

    m_bindedProperties.insert(propertyStr, bindTo);
}

void ShNet::binded_property_changed(const QString &name, const QVariant &value)
{
    Url propertyUrl = m_bindedProperties[name];
    propertyUrl.setArgumentData(value);
    setProperty(propertyUrl);
}

PropertyFilter *ShNet::propertyFilter() const
{
    return m_propertyFilter;
}

// called from PropertyMapper
void ShNet::property_changed(const QString &netName, const QVariant &value)
{  
    sh_net_startmsg();
    out << (quint8)FRAME_TYPE::PROPERY_CHANGE_NOTIFY;
    Url u;
    u.append(UrlNode(UrlNode::NAME, m_nodeName));
    u.append(UrlNode(UrlNode::NAME, netName));
    u.setArgumentType(Url::PROPERTY);
    u.setArgumentData(value);
    out << u;
    sh_net_sendmsg();
}

// called from processDatagram()
void ShNet::set_property(const UrlNode &propertyNode, const QVariant &value)
{
    QHashIterator<QString, property_t> it(m_properties);
    while (it.hasNext()) {
        it.next();
        const property_t &property_entry = it.value();
        if (UrlNode::match(propertyNode, it.key(), property_entry.groups))
            m_propertyMapper->setMappedProperty(it.key(), value);
    }
}

//
// signals & slots
//
void ShNet::addSignal(const QString &name,
                      QObject *source,
                      const char *sourceSignalName)
{
    if (!m_dynamicQObject->connectDynamicSlot(source, sourceSignalName, name))
        qWarning() << "failed to add signal" << name
                   << "to" << m_nodeName << "node";
}

SignalFilter *ShNet::signalFilter() const
{
    return m_signalFilter;
}

// called from DynamicQObject
void ShNet::signal_catched(const QString &name, const QVariantList &parameters)
{
    sh_net_startmsg();
    out << (quint8)FRAME_TYPE::NETWORK_SIGNAL;

    Url u;
    u.append(UrlNode(UrlNode::NAME, m_nodeName));
    u.append(UrlNode(UrlNode::NAME, name));
    out << u;

    out << parameters;
    sh_net_sendmsg();
}

//
// functions
//

void ShNet::addFunction(const QString &name, QObject *object, const char *method, const QStringList &functionGroups)
{
    if (m_functions.contains(name)) {
        qWarning() << "node" << m_nodeName
                   << "already has" << name << "function";
        return;
    }
    m_functions.insert(name, {object, method, functionGroups});
}

void ShNet::execFunction(const Url &url)
{
    sh_net_startmsg();
    out << (quint8)FRAME_TYPE::FUNCTION_CALL;
    out << url;
    sh_net_sendmsg();
}

void ShNet::function_call(const UrlNode &node, const QVariant &arguments,
                          const QString &senderNodeName)
{
    QHashIterator<QString, function_t> it(m_functions);
    QVariant retVal;
    while (it.hasNext()) {
        it.next();
        if (UrlNode::match(node, it.key(), it.value().groups)) {
            bool ok = QMetaObject::invokeMethod(it.value().object, it.value().method,
                                      Q_RETURN_ARG(QVariant, retVal),
                                      Q_ARG(QVariant, arguments));
            if (!ok)
                retVal = QVariant();

            function_exec_result(it.key(), senderNodeName, retVal);
        }
    }
}

void ShNet::function_exec_result(const QString &functionName,
                                 const QString &senderNodeName,
                                 const QVariant &retVal)
{  
    sh_net_startmsg();

    out << (quint8)FRAME_TYPE::FUNCTION_CALL_RESULT;

    Url dest;
    dest.append(UrlNode(UrlNode::NAME, senderNodeName));
    out << dest;

    Url ret;
    ret.append(UrlNode(UrlNode::NAME, m_nodeName));
    ret.append(UrlNode(UrlNode::NAME, functionName));
    ret.setArgumentType(Url::PROPERTY);
    ret.setArgumentData(retVal);
    out << ret;

    sh_net_sendmsg();
}
